package com.ofek.orjsonserializer;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

import com.ofek.applicationbase.utils.IOUtils;


/**
 * <b>Notes about how this works:</b>
 * <br><b>Serialization:</b>
 * <br>1. using reflections it will serialize your object automatically
 * <br>2. all your fields will be converted to a pair field name : field value
 * <br>3. the field name will be changed to same only with a uppercase first letter
 * <br>4. use the annotation JsonField to choose another name to a field
 * <br>5. use JsonIgnore to ignore a field
 * <br>6. use JsonDynamic to use the field's get method to read the value instead of reading the real value
 * <br><b>Deserialization:</b>
 * <br>1. Works for all primitives, arrays, collections, enums and any Objects contains them.
 * <br>2. will throw exception if a type of a field is abstract or interface
 * <br>3. not tested on any other stuff
 * 
 * @author Ofek Ron, Pointer Software Systems.
 *
 */
public class JsonSerializer {
	private static final char OPEN_OBJECT = '{';
	private static final char CLOSE_OBJECT = '}';
	private static final char OPEN_ARRAY = '[';
	private static final char CLOSE_ARRAY = ']';
	private static final char OBJECT_SEPARATOR = ',';
	private static final char ENTRY_SEPARATOR = ':';
	private static final char STRING_INDICATOR = '"';
	private static final String GET = "get";
	
	private HashSet<Object> pathToRoot;
	private NamingPolicy namingPolicy = NamingPolicy.none;
	public enum NamingPolicy {
		none,
		csharp
	}
	
	public JsonSerializer() {
	}
	public JsonSerializer(final NamingPolicy policy) {
		namingPolicy = policy;
	}
	public String serialize(final Object o) throws JsonTypeException, JsonNotTreeException,InvocationTargetException {
		final StringBuilder sb = new StringBuilder(1000);
		pathToRoot = new HashSet<Object>();
		flatObject(o,sb);
		return sb.toString();
	}
	public void serialize(final Object o,final File out) throws Exception {
		BufferedWriter bufferedWriter = null;
		try {
			bufferedWriter = new BufferedWriter(new FileWriter(out));
			final String serialize = serialize(o);
			bufferedWriter.write(serialize);
		} catch (final Exception e) {
			throw e;
		} finally {
			if (bufferedWriter!=null)
				try {
					bufferedWriter.close();
				} catch (final IOException e) {
					e.printStackTrace();
				}
		} 
	}
	private void flatObject(final Object o,final StringBuilder sb) throws JsonTypeException, JsonNotTreeException, InvocationTargetException {
		if (!pathToRoot.add(o)) throw new JsonNotTreeException();
		if (!flatPrimitive(o,sb)) 
			flatGenericObject(o,sb);
		pathToRoot.remove(o);
		
	}
	private boolean flatPrimitive(final Object o,final StringBuilder sb) throws JsonTypeException, JsonNotTreeException, InvocationTargetException {
		if (o instanceof String || o instanceof Boolean  || o instanceof Number) 
			sb.append(wrapString(o.toString()));
		else if (o instanceof Collection) 
			flatCollection(o,sb);
		else if (o.getClass().isArray()) 
			flatArray(o,o.getClass(),sb);
		else if (isEnum(o.getClass())) 
			sb.append(wrapString(((Enum<?>) o).name()));
		else return false;
		return true;
	}
	private boolean isEnum(final Class<? extends Object> class1) {
		if (class1==null) return false;
		return class1.isEnum() || isEnum(class1.getSuperclass());
	}
	
	private void flatGenericObject(final Object o,final StringBuilder sb) throws  JsonTypeException, JsonNotTreeException, InvocationTargetException {
		try {
			sb.append(OPEN_OBJECT);
			handleFields(o,sb);
			handleMethods(o,sb);
			
		} catch (final IllegalArgumentException e) {
			e.printStackTrace();
		} catch (final IllegalAccessException e) {
			e.printStackTrace();
		} catch (final InstantiationException e) {
			e.printStackTrace();
		}
	}
	private void handleMethods(final Object o, final StringBuilder sb) throws IllegalArgumentException, JsonTypeException, JsonNotTreeException, InvocationTargetException, IllegalAccessException {
		final Method[] methods = o.getClass().getMethods();
		for (final Method m : methods) {
				String fieldName = m.getName();
				if (!m.isAnnotationPresent(JsonDynamic.class) || m.isAnnotationPresent(JsonIgnore.class) || m.getReturnType().equals(Void.TYPE) || !m.getName().substring(m.getName().indexOf('.')+1, m.getName().length()).startsWith(GET)) continue;
				if (m.isAnnotationPresent(JsonFieldSpec.class)) fieldName = m.getAnnotation(JsonFieldSpec.class).value();
				final boolean saveAccess = m.isAccessible();
				m.setAccessible(true);
				flatValidField(fieldName, m.invoke(o), sb);
				m.setAccessible(saveAccess);
		}
			
	}
	private void handleFields(final Object o, final StringBuilder sb) throws IllegalArgumentException, JsonTypeException, JsonNotTreeException, InvocationTargetException, IllegalAccessException, InstantiationException {
		final Field[] fields = o.getClass().getDeclaredFields();
		boolean hasFlattenAny = false;
		for (final Field f : fields) 
			if (tryFlatField(o,f,sb)) {
				hasFlattenAny=true;
				sb.append(OBJECT_SEPARATOR);
			}	
		if (hasFlattenAny) sb.deleteCharAt(sb.length()-1);
		sb.append(CLOSE_OBJECT);
	}
	

	private boolean tryFlatField(final Object o, final Field f, final StringBuilder sb) throws JsonTypeException, JsonNotTreeException, InvocationTargetException, IllegalArgumentException, IllegalAccessException, InstantiationException {
		String fieldName = null;
		
		if (f.isAnnotationPresent(JsonIgnore.class) || Modifier.isStatic(f.getModifiers())) return false;
		if (f.isAnnotationPresent(JsonFieldSpec.class)) fieldName = f.getAnnotation(JsonFieldSpec.class).value();
		else switch (namingPolicy) {
			case csharp : 
				final StringBuilder tempsb = new StringBuilder(f.getName());
				tempsb.setCharAt(0, Character.toUpperCase(tempsb.charAt(0)));
				fieldName = tempsb.toString();
			default :
				fieldName = f.getName();
				break;
		}
		final boolean accessible = f.isAccessible();
		f.setAccessible(true);
		final Object value = f.get(o);
		if (value == null) {
			f.setAccessible(accessible);
			return false;
		}
		flatValidField(fieldName,value,sb);
		f.setAccessible(accessible);
		return true;
		
	}


	private String getMethodNameFor(final String name) {
		return GET + Character.toUpperCase(name.charAt(0)) + name.substring(1);
	}

	private void flatValidField(final String name, final Object value,final StringBuilder sb) throws JsonTypeException, JsonNotTreeException, InvocationTargetException {
		sb.append(wrapString(name));
		sb.append(ENTRY_SEPARATOR);
		final StringBuilder sbValue = new StringBuilder();
		flatObject(value,sbValue);
		sb.append(sbValue.toString());
	}

	private String wrapString(final String name) {
		final StringBuilder sb = new StringBuilder();
		sb.append(STRING_INDICATOR).append(StringUtils.escapeString(name)).append(STRING_INDICATOR);
		return sb.toString();
	}

	private void flatCollection(final Object o, final StringBuilder sb) throws JsonTypeException, JsonNotTreeException, InvocationTargetException {
		sb.append(OPEN_ARRAY);
		final Collection<?> collection = (Collection<?>) o;
		final Iterator<?> iterator = collection.iterator();
		while  (iterator.hasNext()) {
			final Object obj = iterator.next();
			flatObject(obj,sb);
			if (iterator.hasNext()) 
				sb.append(OBJECT_SEPARATOR);
		}
		sb.append(CLOSE_ARRAY);
	}
	private void flatArray(final Object o, final Class<?> writeAs, final StringBuilder sb) throws JsonTypeException, JsonNotTreeException, InvocationTargetException {
		sb.append(OPEN_ARRAY);
		final int length = Array.getLength(o);
		if (length != 0) {
			for (int i = 0 ; i < length-1 ; i++ ) {
				flatObject(Array.get(o, i),sb);
				sb.append(OBJECT_SEPARATOR);
			}
			flatObject(Array.get(o, length-1),sb);
		}
		sb.append(CLOSE_ARRAY);
	}
	

	
	public Object deserialize(final String json, final Class<?> clazz) throws Exception {
		final Object primitive = tryGetPrimitive(json,clazz);
		if (primitive!=null) return primitive;
		final Constructor<? extends Object> ctor = clazz.getConstructor();
		final boolean save = ctor.isAccessible();
		ctor.setAccessible(true);
		final Object o = clazz.getConstructor().newInstance();
		ctor.setAccessible(save);
		return flatGenericObject(json,o);
		
	}

	public Object deserialize(final File json, final Class<?> clazz) throws Exception {
		BufferedReader bufferedReader = null;
		try {
			bufferedReader = new BufferedReader(new FileReader(json));
			return deserialize(IOUtils.readAll(json), clazz);
		} catch(final Exception e) {
			throw e; 
		} finally {
			if (bufferedReader!=null)
				try {
					bufferedReader.close();
				} catch (final IOException e) {
					e.printStackTrace();
				}
		}
	}

	private Object flatGenericObject(final String json,final Object o) throws  Exception {
		final JSONParser parser = new JSONParser();
		final Object object = parser.parse(json);
		if (object instanceof JSONArray) throw new IllegalArgumentException("use deserializeArray for arrays, this method can only deserialize json objects");
		if (!(object instanceof JSONObject)) return object;
		final JSONObject jsonObject = (JSONObject)object;
		for (final Field f : o.getClass().getDeclaredFields()) {
			
			final int modifiers = f.getModifiers();
			if (Modifier.isStatic(modifiers)) continue;
			final boolean accessible = f.isAccessible();
			f.setAccessible(true);
			String fieldName;
			if (f.isAnnotationPresent(JsonFieldSpec.class)) {
				fieldName=f.getAnnotation(JsonFieldSpec.class).value();
			} else switch (namingPolicy) {
				case csharp : 
					final StringBuilder sb = new StringBuilder(f.getName());
					sb.setCharAt(0, Character.toUpperCase(sb.charAt(0)));
					fieldName = sb.toString();
					break;
				default: 
					fieldName = f.getName();
					break;
			}
			final Object fieldValue = jsonObject.get(fieldName);
			if (fieldName!=null && fieldValue!=null) 
				handleField(fieldValue.toString(),f,o);
			f.setAccessible(accessible);
		}
		return o;
	}

	private boolean handleField(final String jsonstring, final Field f,final Object o) throws  Exception {
		f.getType();
		if (tryPrimitive(jsonstring,f,o)) return true;
		else if (tryCollection(jsonstring,f, o)) return true;
		else if (tryArray(jsonstring,f,o)) return true;
		else if (tryGenericObject(jsonstring,f,o)) return true;
		return false;
			
	}




	private boolean tryGenericObject(final String jsonstring, final Field f, final Object o) throws Exception {
		f.set(o,deserialize(jsonstring, f.getType()));
		return true;
	}




	private boolean tryPrimitive(final String jsonstring, final Field f, final Object o) throws IllegalArgumentException, IllegalAccessException {
		Object obj;
		if ((obj=tryGetPrimitive(jsonstring,f.getType()))==null) return false;
		f.set(o, obj);
		return true;
	}




	private boolean tryCollection(final String json, final Field f,final Object o) throws Exception {
		if (!isCollection(f.getType())) return false;
		final Type genericFieldType = f.getGenericType();    
		if (genericFieldType instanceof ParameterizedType){
		    final ParameterizedType aType = (ParameterizedType) genericFieldType;
		    final Type[] fieldArgTypes = aType.getActualTypeArguments();
		    f.set(o,deserializeCollection(json,f.getType(),(Class<?>) fieldArgTypes[0]));
		} else return false;
		
		return true;
	}
	public Collection deserializeCollection(final String json, final Class<?> container,final Class<?> contained) throws Exception {
		final JSONParser parser = new JSONParser();
		final JSONArray jsonArray = (JSONArray)parser.parse(json);
		final Collection coll = (Collection) container.newInstance();
		for (final Object obj : jsonArray) {
			final Object primitive = tryGetPrimitive(obj.toString(),contained);
		    if (primitive!=null) coll.add(primitive);
		    else coll.add(deserialize(obj.toString(),contained));
		}
		return coll;
	}
	public Collection deserializeCollection(final File json, final Class<?> container,final Class<?> contained) throws Exception {
		return deserializeCollection(IOUtils.readAll(json),container,contained);
	}
	private boolean tryArray(final String json, final Field f,final Object o) throws Exception {
		Class<?> type = f.getType();
		if (!type.isArray()) return false;
		type = f.getType().getComponentType();
		final JSONParser parser = new JSONParser();
		final JSONArray jsonArray = (JSONArray)parser.parse(json);
		final Object array = Array.newInstance(type, jsonArray.size());
		for (int i = 0; i < jsonArray.size(); i++) {
			final Object value = jsonArray.get(i);
			final Object flatGenericObject = deserialize(value.toString(),type);
			Array.set(array, i, flatGenericObject);
		}  
		f.set(o,array);
		return true;
	}
    

//	private Object fromJsonArray(String json, Field f, Object o) throws Exception {
//		JSONParser parser = new JSONParser();
//		JSONArray jsonArray = (JSONArray)parser.parse(json);
//		Object[] arr;
//		f.set(o,arr=(Object[]) Array.newInstance(f.getType(),jsonArray.size()));
//		for (int i = 0 ; i < jsonArray.size() ; i++) arr[i]=fromJson(jsonArray.get(i), f.);
//		
//		return null;
//	}




	private boolean isCollection(Class<?> type) {
		while ((type=type.getSuperclass()) != null) 
			if (type.equals(Collection.class) || type.equals(AbstractCollection.class)) return true;
		return false;
	}




	private Object tryGetPrimitive(final String s, final Class<?> type) {
		if (type==String.class) return s;
		if (type==Integer.class || type==int.class) return Integer.parseInt(s);
		if (type==Double.class || type==double.class) return Double.parseDouble(s);
		if (type==Long.class || type==long.class) return Long.parseLong(s);
		if (type==Boolean.class || type==boolean.class) return Boolean.parseBoolean(s);
		if (type==Float.class || type==float.class) return Float.parseFloat(s);
		if (type==Short.class || type==short.class) return Short.parseShort(s);
		if (type==Byte.class || type==byte.class) return Byte.parseByte(s);
		if (type==Character.class || type==char.class) return Character.valueOf(s.charAt(0));
		if (type.isEnum()) return Enum.valueOf((Class<Enum>)type, s);
		return null;
	}


}
